Skip to main content

Java Microservices - Beginner's Guide

Table of Contents

  1. What are Microservices?
  2. Monolith vs Microservices
  3. Key Characteristics
  4. Benefits and Challenges
  5. Java Microservices Ecosystem
  6. Core Concepts
  7. Getting Started with Spring Boot
  8. Communication Patterns
  9. Data Management
  10. Service Discovery
  11. Configuration Management
  12. Monitoring and Logging
  13. Security
  14. Deployment Strategies
  15. Best Practices
  16. Common Pitfalls

What are Microservices?

Microservices is an architectural approach where a large application is built as a suite of small, independent services that communicate over well-defined APIs.

Key Points:

  • Each service runs in its own process
  • Services are developed and deployed independently
  • Services can be written in different programming languages
  • Services communicate via HTTP/REST or messaging

Simple Analogy:

Think of a traditional monolithic application like a big apartment building - if you want to change the kitchen, you might affect the entire building. Microservices are like a neighborhood of houses - you can renovate one house without affecting others.


Monolith vs Microservices

AspectMonolithMicroservices
ArchitectureSingle deployable unitMultiple independent services
DatabaseShared databaseDatabase per service
Technology StackSingle technologyMixed technologies allowed
DeploymentDeploy entire applicationDeploy services independently
ScalingScale entire applicationScale individual services
Development TeamSingle teamMultiple small teams
ComplexitySimple initiallyComplex from the start

Key Characteristics

1. Business Capability Focus

Each microservice is built around a specific business capability (e.g., User Management, Payment Processing, Inventory Management).

2. Decentralized Governance

Teams can choose their own technology stack and make independent decisions.

3. Failure Isolation

If one service fails, others continue to operate.

4. Smart Endpoints and Dumb Pipes

Services handle business logic, while communication is simple (HTTP, messaging).

5. Design for Failure

Assume services will fail and design accordingly.


Benefits and Challenges

✅ Benefits

  1. Independent Development & Deployment

    • Teams can work independently
    • Faster release cycles
    • Less coordination overhead
  2. Technology Diversity

    • Choose the right tool for each job
    • Easier to adopt new technologies
  3. Scalability

    • Scale only the services that need it
    • More efficient resource usage
  4. Fault Tolerance

    • Failure in one service doesn't bring down entire system
    • Better resilience

❌ Challenges

  1. Complexity

    • Network calls instead of method calls
    • Distributed system complexity
    • More moving parts
  2. Data Consistency

    • No ACID transactions across services
    • Eventual consistency challenges
  3. Testing

    • Integration testing is harder
    • Need for contract testing
  4. Operational Overhead

    • More services to monitor
    • More deployment pipelines

Java Microservices Ecosystem

Core Frameworks

  • Spring Boot - Most popular Java microservices framework
  • Spring Cloud - Provides microservices patterns
  • Quarkus - Kubernetes-native Java stack
  • Micronaut - Modern JVM-based framework

Supporting Tools

  • Docker - Containerization
  • Kubernetes - Container orchestration
  • Maven/Gradle - Build tools
  • Netflix OSS - Microservices libraries (Eureka, Hystrix)

Core Concepts

1. Service Boundaries

❌ Bad: Services sharing databases
❌ Bad: Services knowing internal details of others
✅ Good: Services with clear, well-defined interfaces
✅ Good: Services owning their data

2. Database per Service

Each microservice should have its own database to ensure loose coupling.

User Service → User DB
Order Service → Order DB
Payment Service → Payment DB

3. API Gateway

Central entry point for all client requests.

Client → API Gateway → [User Service, Order Service, Payment Service]

Getting Started with Spring Boot

1. Basic Microservice Structure

@SpringBootApplication
@RestController
public class UserServiceApplication {

public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}

@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
// Business logic here
return userService.findById(id);
}
}

2. Key Annotations

  • @SpringBootApplication - Main application class
  • @RestController - REST API controller
  • @Service - Business logic layer
  • @Repository - Data access layer
  • @Entity - JPA entity

3. Application Properties

# application.yml
server:
port: 8081

spring:
application:
name: user-service
datasource:
url: jdbc:h2:mem:userdb
driver-class-name: org.h2.Driver

Communication Patterns

1. Synchronous Communication

REST API Calls

@Service
public class OrderService {

@Autowired
private RestTemplate restTemplate;

public User getUserDetails(Long userId) {
String url = "http://user-service/users/" + userId;
return restTemplate.getForObject(url, User.class);
}
}

Feign Client (Declarative)

@FeignClient(name = "user-service")
public interface UserServiceClient {

@GetMapping("/users/{id}")
User getUserById(@PathVariable Long id);
}

2. Asynchronous Communication

Message Queues (RabbitMQ Example)

@RabbitListener(queues = "order.created")
public void handleOrderCreated(OrderCreatedEvent event) {
// Process order created event
emailService.sendOrderConfirmation(event.getOrderId());
}

3. When to Use Each Pattern

PatternUse WhenExample
SynchronousNeed immediate responseGet user profile
AsynchronousFire-and-forget operationsSend email notification

Data Management

1. Database per Service Pattern

✅ Good Pattern:
User Service → MySQL (Users table)
Order Service → PostgreSQL (Orders, OrderItems tables)
Inventory Service → MongoDB (Products collection)

2. Shared Database Anti-Pattern

❌ Avoid This:
User Service ↘
Shared DB
Order Service ↗

3. Data Consistency Patterns

Saga Pattern

For managing transactions across multiple services:

@Service
public class OrderSagaOrchestrator {

public void processOrder(Order order) {
try {
// Step 1: Reserve inventory
inventoryService.reserveItems(order.getItems());

// Step 2: Process payment
paymentService.processPayment(order.getPayment());

// Step 3: Create order
orderService.createOrder(order);

} catch (Exception e) {
// Compensating actions
inventoryService.releaseReservation(order.getItems());
// ... other rollback actions
}
}
}

Service Discovery

1. Netflix Eureka (Spring Cloud)

Eureka Server

@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}

Eureka Client

@EnableEurekaClient
@SpringBootApplication
public class UserServiceApplication {
// Application code
}

2. Application Configuration

# Eureka Client Configuration
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
register-with-eureka: true
fetch-registry: true

Configuration Management

1. Spring Cloud Config

Config Server

@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}

Config Client

# bootstrap.yml
spring:
cloud:
config:
uri: http://localhost:8888
application:
name: user-service

2. Environment-Specific Configuration

config-repo/
├── user-service.yml # Default config
├── user-service-dev.yml # Development config
├── user-service-prod.yml # Production config
└── application.yml # Global config

Monitoring and Logging

1. Distributed Tracing

// Spring Cloud Sleuth automatically adds tracing
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
log.info("Getting user with id: {}", id); // Automatically traced
return userService.findById(id);
}

2. Health Checks

@Component
public class DatabaseHealthIndicator implements HealthIndicator {

@Override
public Health health() {
if (isDatabaseUp()) {
return Health.up().withDetail("database", "Available").build();
} else {
return Health.down().withDetail("database", "Not Available").build();
}
}
}

3. Metrics with Micrometer

@RestController
public class UserController {

private final Counter userRequestCounter;

public UserController(MeterRegistry meterRegistry) {
this.userRequestCounter = Counter.builder("user.requests")
.description("Number of user requests")
.register(meterRegistry);
}

@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
userRequestCounter.increment();
return userService.findById(id);
}
}

Security

1. OAuth2 with JWT

@EnableWebSecurity
@EnableResourceServer
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.jwt();
}
}

2. Service-to-Service Authentication

@Configuration
public class FeignClientConfig {

@Bean
public RequestInterceptor requestTokenBearerInterceptor() {
return requestTemplate -> {
String token = getCurrentUserToken();
requestTemplate.header("Authorization", "Bearer " + token);
};
}
}

Deployment Strategies

1. Docker Containerization

FROM openjdk:11-jre-slim

COPY target/user-service-1.0.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "/app.jar"]

2. Kubernetes Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
spec:
replicas: 3
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: user-service:1.0
ports:
- containerPort: 8080

Best Practices

1. Start with a Monolith

  • Build a monolith first
  • Extract microservices when you understand the domain boundaries

2. Service Size

  • Follow the "two-pizza team" rule
  • If a team can't be fed with two pizzas, the service might be too big

3. API Design

// ✅ Good: Versioned APIs
@GetMapping("/v1/users/{id}")
public User getUserV1(@PathVariable Long id) { ... }

@GetMapping("/v2/users/{id}")
public UserV2 getUserV2(@PathVariable Long id) { ... }

4. Error Handling

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException e) {
ErrorResponse error = new ErrorResponse("USER_NOT_FOUND", e.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}

5. Circuit Breaker Pattern

@Component
public class UserServiceClient {

@CircuitBreaker(name = "user-service", fallbackMethod = "fallbackUser")
public User getUser(Long id) {
return restTemplate.getForObject("/users/" + id, User.class);
}

public User fallbackUser(Long id, Exception ex) {
return new User(id, "Unknown User"); // Fallback response
}
}

Common Pitfalls

1. Distributed Monolith

Problem: Services are too tightly coupled ✅ Solution: Ensure services can be developed and deployed independently

2. Chatty Interfaces

Problem: Too many API calls between services ✅ Solution: Design coarser-grained APIs

3. Shared Database

Problem: Multiple services accessing the same database ✅ Solution: Database per service pattern

4. Ignoring Network Latency

Problem: Treating remote calls like local calls ✅ Solution: Design for network failures and latency

5. Not Monitoring Enough

Problem: Lack of observability in distributed system ✅ Solution: Comprehensive monitoring, logging, and tracing


Learning Path

Phase 1: Foundations

  1. Learn Spring Boot basics
  2. Understand REST API design
  3. Practice with simple CRUD applications

Phase 2: Microservices Basics

  1. Create multiple Spring Boot services
  2. Implement service-to-service communication
  3. Set up service discovery with Eureka

Phase 3: Advanced Patterns

  1. Implement API Gateway
  2. Add configuration management
  3. Set up monitoring and logging

Phase 4: Production Ready

  1. Add security (OAuth2/JWT)
  2. Implement circuit breakers
  3. Set up containerization and orchestration

Useful Resources

Documentation

Books

  • "Microservices Patterns" by Chris Richardson
  • "Building Microservices" by Sam Newman
  • "Spring Microservices in Action" by John Carnell

Tools to Explore

  • Docker - Containerization
  • Kubernetes - Container orchestration
  • Postman - API testing
  • Zipkin - Distributed tracing
  • Prometheus - Monitoring
  • ELK Stack - Logging

Summary

Microservices architecture offers many benefits but comes with increased complexity. Start small, learn the patterns, and gradually build up your understanding. Remember:

  1. Domain-driven design is crucial for service boundaries
  2. Automation is essential for managing complexity
  3. Monitoring is critical for distributed systems
  4. Team structure should align with service architecture
  5. Start simple and evolve your architecture over time

The key to successful microservices is not the technology, but understanding the business domain and designing services around business capabilities.